#region using

using System;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using System.Web;
using System.IO;
using System.Text;
using System.Collections.Generic;

#endregion

/// <summary>
/// Converts ASCII smilies into real smilies in the comments.
/// This class is for BlogEbgine.net, not for stand-alone app.
/// </summary>
/// <remarks>
/// More fun in store
/// </remarks>
[Extension("Improves post typogrpaphy", "1.3", "<a href=\"http://nesteruk.org\">Dmitri Nesteruk</a>")]
public class TypograFix
{
  /// <summary>
  /// Listen to posts being served.
  /// </summary>
  static TypograFix()
  {
    Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
  }

  /// <summary>
  /// The event handler that is triggered every time a comment is served to a client.
  /// </summary>
  /// <remarks>
  /// Exceptions are handled lazily at the moment.
  /// </remarks>
  private static void Post_Serving(object sender, ServingEventArgs e)
  {
    if (!string.IsNullOrEmpty(e.Body))
    {
      try
      {
        e.Body = TypografixTransform(HttpUtility.HtmlDecode(e.Body))
                 // + "<pre>" + HttpUtility.HtmlEncode(HttpUtility.HtmlDecode(e.Body)) + "</pre>"
                 ;
      }
      catch (Exception ex)
      {
        // todo: report when admin
        e.Body += " Exception: " + ex.Message;
      }
    }
  }

  #region Production
  /// <summary>
  /// The tags in which text cannot be transformed.
  /// </summary>
  private static readonly string[] unsuitable = { "code", "pre" };

  /// <summary>
  /// This stack stores a list of tag we're in.
  /// </summary>
  private static Stack<string> context;

  /// <summary>
  /// Returns <c>true</c> if replacement is forbidden in given context.
  /// </summary>
  /// <value><c>true</c> if cannot replace; otherwise, <c>false</c>.</value>
  private static bool cannotReplace
  {
    get
    {
      // code or pre elements get banned
      foreach (string s in context)
      { // can't even use LINQ :(
        if (Array.IndexOf(unsuitable, s.ToLower()) >= 0)
          return true;
      }
      return false;
    }
  }

  /// <summary>
  /// This function actually alters the text and makes the necessary changes.
  /// </summary>
  /// <param name="text">Initial text, with HTML entities decoded (use <see cref="T:System.Web.HttpUtility"/>).</param>
  /// <returns>The altered text.</returns>
  /// <remarks>Contains tracing until things work perfectly.</remarks>
  /// <exception cref="Exception">Will throw exception if source HTML is malformed.</exception>
  private static string TypografixTransform(string text)
  {
    StringBuilder hb = new StringBuilder();
    context = new Stack<string>();
    Stack<char> quoteStack = new Stack<char>();
    for (int i = 0; i < text.Length; ++i)
    {
      switch (text[i])
      {
        #region <

        case '<':
          hb.Append('<');
          StringBuilder tb = new StringBuilder();
          while (++i < text.Length)
          {
            hb.Append(text[i]);
            if (text[i] == '\"')
            {
              // this (opening quote) has been appended
              // append everything until we meet a closing one
              do
              {
                ++i;
                hb.Append(text[i]);
              } while (text[i] != '\"');
            }
            else if (char.IsWhiteSpace(text[i]))
            {
              // we're in this tag's context
              context.Push(tb.ToString());
            }
            else if (text[i] == '/')
            {
              tb = new StringBuilder();
              string s = context.Pop();
              // move until we meet > and add it
              while (++i < text.Length)
              {
                if (text[i] == '>')
                  goto default;
                hb.Append(text[i]);
                Console.WriteLine("Appended internally " + text[i]);
              }
            }
            else if (text[i] == '>')
            {
              // if closed, break this loop
              context.Push(tb.ToString());
              break;
            }
            else
              tb.Append(text[i]);
          }
          break;

        #endregion

        #region -

        case '-':
          if (cannotReplace) goto default;
          // if surrounded by spaces, it's an en dash
          if (i > 0 && i + 1 < text.Length && text[i - 1] == ' ' && text[i + 1] == ' ')
            hb.Append("&ndash;");
          else if (i + 1 < text.Length && text[i + 1] == '-' &&
                   (i + 2 == text.Length || text[i + 2] != '&'))
          {
            hb.Append("&mdash;");
            ++i; // ignore the second dash
          }
          else if (i + 5 < text.Length &&
                   text[i + 1] == '-' &&
                   text[i + 2] == '&' &&
                   text[i + 3] == 'g' &&
                   text[i + 4] == 't' &&
                   text[i + 5] == ';')
          {
            hb.Append("&rarr;");
            i += 5;
          }
          else goto default;
          // same for em dash
          break;

        #endregion

        #region (

        case '(':
          if (cannotReplace) goto default;
          if (i + 2 < text.Length && text[i + 2] == ')')
          {
            if (char.ToLower(text[i + 1]) == 'c')
            {
              hb.Append("&copy;");
              i += 2;
            }
            else if (char.ToLower(text[i + 1]) == 'r')
            {
              hb.Append("&reg;");
              i += 2;
            }
            else goto default;
          }
          else if (i + 3 < text.Length && char.ToLower(text[i + 1]) == 't'
                   && char.ToLower(text[i + 2]) == 'm'
                   && char.ToLower(text[i + 3]) == ')')
          {
            hb.Append("&#8482;");
            i += 3;
          }
          else goto default;
          break;

        #endregion

        #region '

        case '\'': // single quotes
          if (cannotReplace) goto default;

          // if there's no space on the right, force closing
          if (i > 0 && i + 1 < text.Length
              && (char.IsLetterOrDigit(text[i - 1]))
              && (char.IsLetterOrDigit(text[i + 1])))
          {
            hb.Append("&rsquo;"); // stack ignored
          }
          else if (quoteStack.Count > 0 && quoteStack.Peek() == '\'')
          {
            hb.Append("&rsquo;");
            quoteStack.Pop();
          }
          else
          {
            hb.Append("&lsquo;");
            quoteStack.Push('\'');
          }
          break;

        #endregion

        #region "

        case '\"': // double quotes
          if (cannotReplace) goto default;
          // if there is a quote on the stack, close it
          if (quoteStack.Count > 0 && quoteStack.Peek() == '\"')
          {
            hb.Append("&rdquo;");
            quoteStack.Pop();
          }
          else
          {
            hb.Append("&ldquo;");
            quoteStack.Push('\"');
          }
          break;

        #endregion

        #region .

        case '.': // possible ellipsis
          if (i + 2 < text.Length && text[i + 1] == '.' && text[i + 2] == '.')
          {
            if (i > 0 && i + 3 < text.Length)
            {
              char before = text[i - 1];
              char after = text[i + 3];
              if (Environment.NewLine.IndexOf(before) >= 0 &&
                  Environment.NewLine.IndexOf(after) >= 0)
              {
                hb.Append("&#8942;");
                i += 2;
                continue;
              }
            }
            if (cannotReplace) goto default;
            hb.Append("&hellip;");
            i += 2;
          }
          else goto default;
          break;

        #endregion

        #region x

        case 'x':
          if (cannotReplace) goto default;
          if (i > 0 && char.IsDigit(text[i - 1])
              &&
              ((
                 i + 1 < text.Length &&
                 (char.IsDigit(text[i + 1]) || char.IsWhiteSpace(text[i + 1]) || text[i + 1] == '<')
               ) || (i + 1 == text.Length))
            )
          {
            hb.Append("&times;");
          }
          else goto default;
          break;

        #endregion

        #region <

        case '&':
          if (cannotReplace) goto default;
          if (i + 5 < text.Length)
          {
            if (text[i + 1] == 'l' && text[i + 2] == 't' && text[i + 3] == ';')
            {
              if (text[i + 4] == '-' && text[i + 5] == '-')
              {
                hb.Append("&larr;");
                i += 5;
              }
              else if (text[i + 4] == '=' && text[i + 5] == '=')
              {
                hb.Append("&lArr;");
                i += 5;
              }
              else goto default;
            }
            else goto default;
          }
          else goto default;
          break;

        #endregion

        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '0':
          {
            // keep accumulating while digit
            int j;
            StringBuilder acc = new StringBuilder();
            for (j = i; j < text.Length; ++j)
            {
              if (char.IsDigit(text[j]) || text[j] == ',')
                acc.Append(text[j]);
              else break;
            }
            if (j + 1 >= text.Length)
              goto default;
            // st
            char lastChar = acc.ToString()[acc.ToString().Length-1];
            if (lastChar == '1' && text[j] == 's' && text[j + 1] == 't')
            {
              hb.Append(acc + "<sup>st</sup>");
              i += acc.Length + 1;
            }
            else if (lastChar == '2' && text[j] == 'n' && text[j + 1] == 'd')
            {
              hb.Append(acc + "<sup>nd</sup>");
              i += acc.Length + 1;
            }
            else if (lastChar == '3' && text[j] == 'r' && text[j + 1] == 'd')
            {
              hb.Append(acc + "<sup>rd</sup>");
              i += acc.Length + 1;
            }
            else if (text[j] == 't' && text[j + 1] == 'h')
            {
              hb.Append(acc + "<sup>th</sup>");
              i += acc.Length + 1;
            }
            else goto default;
          }
          break;

        default:
          hb.Append(text[i]);
          break;
      }
    }
    return hb.ToString();
  }
  #endregion
}